package squareBorderPackage;

import java.math.*;
import java.util.*;

import temperaturePackage.*;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of Squares involving a temperature, a humidity degree and borders.
 * 
 * @invar 	Each Square must have proper borders attached to it.
 *        	| hasProperBorders()
 * @invar	The borders of each square must be appropriate.
 * 			| areAppropriateBorders(getBorders())	
 * 
 * @version 4.0
 * @author Jessa Bekker & Esther Alberts
 */
public abstract class Square {

	/**
	 * Initialize this new square with non-slippery walls in the given directions. 
	 *  
	 * @param 	wallOrDoorDirections
	 * 			The directions in which a (non-slippery)wall or door must be installed.
	 * @param	areWalls
	 * 			Says whether there must be walls or doors in the given wallOrDoorDirections.
	 * @post	The borders in the given directions are walls if areWalls is true and else they are doors.
	 * 			| for each direction in wallOrDoorDirections:
	 * 			|	if(areWalls == true)
	 * 			| 		new.getBorderAt(direction) instanceOf Wall
	 * 			|	else
	 * 			|		new.getBorderAt(direction) instanceOf Door
	 * @post	For each direction except for the wallOrDoorDirection, an openBorder is set.
	 * 			|for each direction in getListOfDirections:
	 * 			| if(!wallOrDoorDirection.contains(direction))
	 * 			|	new OpenBorder(this, direction)
	 * @post	This new square is not yet terminated.
	 * 			| ! new.isTerminated()
	 */
	@Raw
	public Square(boolean areWalls, Direction...wallOrDoorDirections){
		borders = new HashMap<Direction, Border>();
		obstacles = new HashMap<Direction, Boolean>();
		List<Direction> directions = Direction.getListOfDirections();
		if(wallOrDoorDirections.length == 0){
			wallOrDoorDirections = new Direction[1];
			wallOrDoorDirections[0] = Direction.FLOOR;
		}
		for(Direction direction: wallOrDoorDirections){
			directions.remove(direction);
			if(areWalls == true)
				new Wall(false, this, direction);
			else
				new Door(false, this, direction);
		}
		for(Direction direction: directions)
			new OpenBorder(this, direction);
	}
	
	/**
	* Terminate this square and its associated borders if this is their only square.
	*
	* @post 	This square is terminated.
	* 			| new.isTerminated()
	* @effect 	Each border associated with this square is removed as border of this square.
	* 			| for each direction in Direction.getListOfDirections():
	* 			| getBorders().get(direction).removeASquare(this)
	*/
	public void terminate() {
		if (!isTerminated()) {
			for (Direction direction : Direction.getListOfDirections())
				if(hasBorderAt(direction))
					getBorderAt(direction).removeSquareAt(direction);
			this.isTerminated = true;
		}
	}
	
	/**
	 * Check whether this square is terminated.
	 */
	@Basic @Raw
	public boolean isTerminated() {
		return this.isTerminated;
	}

	/**
	 * Variable registering whether this square is terminated.
	 */
	private boolean isTerminated;

	/**
	 * Check whether this square is in a dungeon.
	 * 
	 * @return True if and only if this square is in a dungeon.
	 */
	@Basic
	public boolean isInDungeon() {
		return isInDungeon && !isTerminated();
	}

	/**
	 * Confirm this square to be in a dungeon.
	 * 
	 * @post	This square is said to be in a dungeon until it is terminated.
	 * 			| new.isInDungeon() != isTerminated()
	 */
	public void inDungeon() {
		this.isInDungeon = true;
	}

	/**
	 * variable registering whether this square is in a dungeon.
	 */
	private boolean isInDungeon = false;
	
	/**
	 * Return the border in the given direction.
	 * 	A null reference is returned if no border is attached to
	 *	this square in the given direction.
	 */
	@Basic @Raw
	public Border getBorderAt(Direction direction) {
		return borders.get(direction);
	}

	/**
	 * Return the map of borders.
	 */
	@Basic
	public HashMap<Direction, Border> getBorders() {
		return new HashMap<Direction, Border>(borders);
	}

	/**
	 * Return the border which has this square and the given other square as its squares.
	 * 	A null reference is returned if these squares do not share a border.
	 * 
	 * @param 	other
	 * 			The other square of the returned border
	 * @return	Return the border who has this square and the given other square as its squares.
	 * 			|for each border in getBorders().values():
	 * 			|	if(border.hasAsSquare(other))
	 * 			|		return border
	 * @return	Return null if this square and the given square have no border in common.
	 * 			|if(for each border in getBorders().values():!border.hasAsSquare(other))
	 * 			|		return null
	 */
	public Border getSharedBorder(Square other) {
		for (Border border : getBorders().values())
			if (border.hasAsSquare(other))
				return border;
		return null;
	}

	/**
	 * Check whether this square has a border attached to it in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction to check for a border;
	 * @return	True if and only if this square references an effective border
	 * 			in the given direction.
	 * 			| result == getBorderAt(direction) != null
	 */
	public boolean hasBorderAt(Direction direction){
		return getBorderAt(direction) != null;
	}
	
	/**
	 * Check whether there is at least one door or wall in the borders of this square.
	 * 
	 * @param 	borders
	 * 			The borders to check.
	 * @return	True if and only if some of the given borders are walls or doors.
	 * 			| result == (for some border of borders.values() :
	 * 			|	border instanceof Wall || border instanceof Door) 
	 */
	public boolean containsWallOrDoor(HashMap<Direction, Border> borders){
		for(Border border : borders.values())
			if(border instanceof Wall || border instanceof Door)
				return true;
		return false;
	}

	/**
	 * Check whether this square can have the given border at the given direction
	 * 
	 * @param 	direction
	 * 			The direction to check
	 * @param 	border
	 * 			The border to check
	 * @return	True if the border is not effective
	 * 			|if(border == null)
	 *			| 	result == true
	 *@return	True if the given border has the given direction in its squareDirections
	 *			|if(border.getSquareDirections()[0].equals(direction)
	 *			|		|| border.getSquareDirections()[1].equals(direction))
	 *			|	result == true
	 */
	public boolean canHaveAsBorderAt(Direction direction, Border border) {
		if (border == null)
			return true;
		return (border.getSquareDirections()[0].equals(direction)
				|| border.getSquareDirections()[1].equals(direction));
	}

	/**
	 * Check whether this square has a valid combination of Borders
	 * 
	 * @param 	borders
	 * 			The borders to check.
	 * @return	False if the given hash map is not effective.
	 * 			|if(borders == null)
	 * 			|	result == false
	 * @return	False if the given hash map has no valid numbers of doors.
	 * 			|if(!hasValidNbOfDoors(borders))
	 * 			|	result == false
	 * @return	False if the given hash map does not contain any
	 * 			walls or doors
	 * 			|if(!containsWallOrDoor(borders))
	 * 			|	result == false
	 * @return	False if there is a border in one of the directions which is not valid
	 * 			|result != for some direction in Direction.getListOfDiretions()
	 * 			|			!canHaveAsBorderAt(direction, borders.get(direction)	
	 */
	public boolean areAppropriateBorders(HashMap<Direction, Border> borders) {
		if(borders == null)
			return false;
		for(Direction direction : Direction.getListOfDirections())
			if(!canHaveAsBorderAt(direction, borders.get(direction)))
				return false;
		if(!hasValidNbOfDoors(borders))
			return false;
		if(!containsWallOrDoor(borders))
			return false;
		return true;
	}

	/**
	 * Check whether this square has proper borders attached to it.
	 * 
	 * @return	True if and only if in all directions this square does
	 * 			not reference an effective border, or if the border
	 * 			referenced by this square in that direction in turn
	 * 			references this square as the square to which it is attached.
	 * 			| if (for some Direction in Direction.getListOfDirections() :
	 * 			|	!(getBorderAt(direction ) == null 
	 * 			|		|| getBorderAt(direction).getSquareAt(direction) == this))
	 * 			| then (result == false)
	 * 			| else (result == true)
	 */
	@Raw
	public boolean hasProperBorders() {
		for(Direction direction: Direction.getListOfDirections())
			if (!(getBorderAt(direction ) == null || getBorderAt(direction).getSquareAt(direction) == this))
				return false;
		return true;
	}
	
	/**
	 * Set the border attached to this square in the given direction to the given border
	 *  
	 * @param 	border
	 * 			The border to be attached to this square.
	 * @param 	direction
	 * 			The direction to attach the given border to.
	 * @pre		If the given border is effective, it must can add a square in the given direction
	 * 			or it must already reference this square in the given direction.
	 * 			| if(border != null)
	 * 			|	then (border.getDirectionOfAttachement() == direction 
	 * 			|		|| border.getSquareAt(direction) == this)
	 * @pre		If the given border is not effective and this square has a border
	 * 			attached to it in the given direction, that border may not reference
	 * 			this square as the square to which it is attached in the given direction.
	 * 			| if ( (border == null) && (square.hasBorderAt(direction) )
	 * 			|	then getBorderAt(direction).getSquareAt(direction) != this
	 * @post	This square references the given border as the border attached
	 * 			to it in the given direction.
	 * 			| new.getBorderAt(direction) == border
	 */
	@Raw
	void setBorderAt(@Raw Border border, Direction direction) {
		assert (border == null) || (border.getSquareAt(direction) == this);
		assert (border != null) || (!hasBorderAt(direction)) ||
				(getBorderAt(direction).getSquareAt(direction) != this);
		borders.put(direction, border);
		if(border == null){
			obstacles.remove(direction);
		}
		else
			setObstacleAt(direction);
	}

	/**
	 * Variable registering the borders of a square. The map holds as keys
	 * directions and as values borders, so it says in which direction which
	 * kind of border is placed.
	 */
	private HashMap<Direction, Border> borders;

	/**
	 *  Check whether the given borders have no more than maxDoors values of type "Door"
	 * 
	 * @param	borders
	 * 			The borders to check
	 * @return	Return false if there are more than maxDoors values of type 
	 * 			"Door" in the borders.
	 *			|if (getNumberOfDoors(borders) > maxDoors)
	 *	 		|	result==false
	 */
	public boolean hasValidNbOfDoors(HashMap<Direction, Border> borders) {
		setMaxDoors(3);
		int i = getNumberOfDoors(borders);
		if (i > getMaxDoors())
			return false;
		return true;
	}
	
	/**
	 * Get the number of values of type Door in the given borders.
	 * 
	 * @param 	borders
	 * 			The borders to get the number of doors of.
	 */
	public int getNumberOfDoors(HashMap<Direction, Border> borders){
		int i = 0;
		for (Border border : borders.values()) {
			if (border instanceof Door)
				i++;
		}
		return i;
	}

	/**
	 * Return the minimum number of values of type door in borders.
	 */
	@Basic
	protected int getMinDoors(){
		return minDoors;
	}
	
	/**
	 * Set the minimum of doors possible for this square.
	 * 
	 * @param 	nbMinDoors
	 * 			The min number of doors possible for this square.
	 * @post	The minDoors of this square will be equal to the given nbMinDoors.
	 * 			|this.minDoors = nbMinDoors
	 */
	protected void setMinDoors(int nbMinDoors){
		this.minDoors = nbMinDoors;
	}

	/**
	 * Variable registering the minimum number of values of type "Door" in borders.
	 */
	private int minDoors;
	
	/**
	 * Return the maximum number of values of type door in borders.
	 */
	@Basic
	protected int getMaxDoors(){
		return maxDoors;
	}
	
	/**
	 * Set the maximum of doors possible for this square.
	 * 
	 * @param 	nbMaxDoors
	 * 			The max number of doors possible for this square.
	 * @post	The maxDoors of this square will be equal to the given nbMaxDoors.
	 * 			|this.maxDoors = nbMaxDoors
	 */
	protected void setMaxDoors(int nbMaxDoors){
		this.maxDoors = nbMaxDoors;
	}
	
	/**
	 * Variable registering the maximum number of values of type "Door" in borders.
	 */
	private int maxDoors;
	
	/**
	 * Return whether there is an obstacle in the given direction.
	 */
	public boolean getObstacleAt(Direction direction) {
		return obstacles.get(direction);
	}

	/**
	 * Return the listOfObstacles of this square.
	 */
	public HashMap<Direction, Boolean> getObstacles() {
		return obstacles;
	}

	/**
	 * Adjust the listOfObstacles if a new border is added to the borders at given direction.
	 * 
	 * @param 	direction
	 *          The direction in which the border is set.
	 * @post	The list of obstacles is updated in the given direction
	 * 			| new.getObstacleAt(direction) == getBorderAt(direction).isObstacle() 
	 */
	private void setObstacleAt(Direction direction) {
		obstacles.put(direction, getBorderAt(direction).isObstacle());
	}
	
	/**
	 * Variable registering for each direction whether there is a obstacle or
	 * not. The map holds as keys directions and as values booleans, for which
	 * true means obstacle.
	 */
	private HashMap<Direction, Boolean> obstacles;

	/**
	 * Return a set containing all the squares that are in the space of this square.
	 */
	public abstract Set<? extends Square> getSpace();
	
	/**
	 * Return the number of squares in the space of this square.
	 * 
	 * @return 	the number of squares in the space of this square.
	 * 			|getSpace().size() 
	 */
	public abstract int getNbOfSquaresInSpace();

	/**
	 * check whether the given square is in the same space as this square.
	 * 
	 * @param 	other
	 * 			The square to check the space for.
	 * @return	True if and only if the given square is effective and is in
	 * 			the space of this square.
	 * 			| result = (other != null && !isTerminated && getSpace().contains(other))
	 */
	public boolean isInSameSpace(Square other) {
		return (other != null && !isTerminated() && getSpace().contains(other));
	}

	/**
	 * Update the space of this square.
	 */
	public abstract void updateSpace();

	/**
	 * Return the temperature of this square.
	 */
	@Basic @Raw
	public abstract Temperature getTemperature();

	/**
	 * Return the coldDamage of this square.
	 * 
	 * @return	1 point per cold damage constant under the maximum temperature for cold damage.
	 * 			if the temperature is lower than the maximum temperature for cold damage, otherwise 0.
	 * 			| if (getTemperature().toUnit(TemperatureUnit.CELSIUS).getNumeral() < MAX_COLD)
	 * 			|	result == -((getTemperature().toUnit(TemperatureUnit.CELCIUS).getNumeral() - MAX_COLD)/COLD_CTE)modCOLD_CTE + 1
	 * 			| else
	 * 			|	result == 0 
	 */
	@Raw
	public int getColdDamage() {
		int coldDamage = 0;
		double temperatureDouble = -(getTemperature().toUnit(
				TemperatureUnit.CELSIUS).getNumeral() - MAX_COLD);
		if (temperatureDouble > 0) {
			temperatureDouble = temperatureDouble / COLD_CTE;
			coldDamage = (int) temperatureDouble + 1;
		}

		return coldDamage;
	}

	/**
	 * Variable registering the the maximum temperature in Celsius for having
	 * cold damage.
	 */
	private static final double MAX_COLD = -5;

	/**
	 * Variable registering the cold damage constant in Celsius
	 */
	private static final int COLD_CTE = 10;

	/**
	 * Return the heatDamage of this square.
	 * 
	 * @return 	1 point per heat damage constant above the minimum temperature for heat damage.
	 * 			if the temperature is higher than the minimum temperature for heat damage, otherwise 0.
	 * 			| if (getTemperature().toUnit(TemperatureUnit.CELSIUS).getNumeral() < minHeat)
	 * 			|	result == ((getTemperature().toUnit(TemperatureUnit.CELCIUS).getNumeral() - minHeat)/heatCte)mod heatCte + 1
	 * 			| else
	 * 			|	result == 0 
	 */
	@Raw
	public int getHeatDamage() {
	
		int heatDamage = 0;
		double temperatureDouble = getTemperature().toUnit(
				TemperatureUnit.CELSIUS).getNumeral() - getMinHeat();
		if (temperatureDouble > 0) {
			temperatureDouble = temperatureDouble / getHeatCte();
			heatDamage = (int) temperatureDouble + 1;
		}
	
		return heatDamage;
	}

	/**
	 * Return the minimum temperature in Celsius for having heat damage.
	 */
	@Basic @Raw
	public double getMinHeat() {
		return minHeat;
	}

	/**
	 * set the minimum temperature in Celsius for having heat damage for all
	 * objects of Square.
	 * 
	 * @param 	minHeatCte
	 *          The minimum temperature in Celsius for having heat damage.
	 * 
	 */
	@Raw
	public static void setMinHeat(double minHeat) {
		Square.minHeat = minHeat;
	}

	/**
	 * Variable registering the minimum temperature in Celsius for having heat damage.
	 */
	private static double minHeat = 35;

	/**
	 * Return the heat constant in Celsius.
	 */
	@Basic @Raw
	public int getHeatCte() {
		return heatCte;
	}

	/**
	 * set the heat constant in Celsius for all objects of Square.
	 * 
	 * @param 	heat
	 *        	The heat constant in Celsius.
	 */
	@Raw
	public static void setHeatCte(int heat) {
		heatCte = heat;
	}

	/**
	 * Variable registering the heat constant in Celsius.
	 */
	private static int heatCte = 15;

	/**
	 * Returns the humidity degree of this square.
	 */
	@Basic @Raw
	public abstract BigDecimal getHumidity();

	
	
	/**
	 * Returns the rust damage of this square. The rust damage is a rate of how
	 * much metal objects in this square are damaged by rust.
	 * 
	 * @return	1 point per rust constant above the minimum humidity for rust
	 * 			if humidity is above the minimum for rust, otherwise 0.
	 * 			| if (getHumidity().doubleValue() > getMinRust())
	 *		 	|	result == ((getHumidity().doubleValue() - getMinRust())/getRustCte()) mod getRustCte() + 1
	 * 			| else
	 *		 	|	result == 0
	 */
	@Raw
	public int getRustDamage() {
		int rustDamage = 0;
		double rustDouble = getHumidity().doubleValue() - getMinRust();
		if (rustDouble > 0) {
			rustDouble = rustDouble / getRustCte();
			rustDamage = (int) rustDouble + 1;
		}
	
		return rustDamage;
	}

	/**
	 * Return the minimum humidity degree for having rust damage.
	 */
	@Basic @Raw
	public double getMinRust() {
	return minRust;
	}

	/**
	 * set the minimum humidity degree for having rust damage for all objects of Square.
	 * 
	 * @param 	minRustCte
	 *          The minimum humidity degree for having rust damage.
	 */
	@Raw
	public static void setMinRust(double minRustCte) {
		minRust = minRustCte;
	}

	/**
	 * Variable registering the minimum humidity degree for having rust damage.
	 */
	private static double minRust = 30;

	/**
	 * Return the rust constant.
	 */
	@Basic @Raw
	public int getRustCte() {
		return rustCte;
	}

	/**
	 * set the rust constant for all objects of Square.
	 * 
	 * @param 	rust
	 *          The rust constant.
	 */
	@Raw
	public static void setRustCte(int rust) {
		rustCte = rust;
	}

	/**
	 * Variable registering the rust constant.
	 */
	private static int rustCte = 7;

	/**
	 * Return true if and only if the flooring of this square is slippery
	 */
	@Basic @Raw
	public boolean getSlipperyFlooring() {
		Border floor = getBorderAt(Direction.FLOOR);
		return floor.getSlippery();
	}

	/**
	 * Return a boolean reflecting whether the floor of this Square is slippery.
	 * 
	 * @return	True if the flooring of this square is slippery.
	 * 			| if (getFlooring() == true)
	 * 			|	result == true
	 * 	@return	True if the temperature in Celsius is strictly positive and the humidity is equal to 100.
	 * 			| if (getTemperature().toUnit(TemperatureUnit.CELSIUS).getNumeral() > 0
	 * 			|	&& getHumidity().compareTo(new BigDecimal(100)) == 0)
	 * 			|	result == true
	 * @return	True if the temperature in Celsius is negative or equal to zero and the humidity
	 * 				is larger than  10.
	 * 			| if (getTemperature().toUnit(TemperatureUnit.CELSIUS).getNumeral() =< 0
	 * 			|	&& getHumidity().compareTo(BigDecimal.TEN) > 0)
	 * 			|	result == true;
	 */
	@Raw
	public boolean isSlippery() {
		if (getSlipperyFlooring())
			return true;
		if (getTemperature().toUnit(TemperatureUnit.CELSIUS).getNumeral() > 0) {
			if (getHumidity().compareTo(new BigDecimal(100)) == 0)
				return true;
		} 
		else if (getHumidity().compareTo(BigDecimal.TEN) > 0)
			return true;
		return false;
	}

	/**
	 * Return the degree of inhabitability of this square.
	 * 
	 * @return	If this square has nor cold damage, nor heat damage, the inhabitability
	 * 			is equal to zero.
	 * 			| if(this.getColdDamage() == 0 && this.getHeatDamage() == 0)
	 * 			|	result == 0; 
	 * @return	If this square has cold damage, the inhabitability is the negative square
	 * 			of its cold damage.
	 * 			| if (this.getColdDamage != 0)
	 * 			|	result == - Math.sqrt(getColdDamage());
	 * @return	If this square has heat damage, the inhabitability is the negative square
	 * 			of the heat damage to the power of 3, divided by the square of 101 minus the humidity.
	 * 			| if (this.getHeatDamage !=0)
	 * 			|	result == - Math.sqrt(Math.pow(getHeatDamage(),3))/Math.sqrt(101 - getHumidity().doubleValue())
	 * 	
	 */
	@ Raw
	public double getInhabitability() {
	
	double heat = Math.sqrt(Math.pow(getHeatDamage(), 3));
	double cold = Math.sqrt(getColdDamage());
	double hum = Math.sqrt(101 - getHumidity().doubleValue());
	
	return -1*heat/hum -cold;
	}
}